package com.sinevil.tcpTrans.utils.fileTrans.fileDownload;

import com.sinevil.tcpTrans.entity.json.FilePacketJson;
import com.sinevil.tcpTrans.entity.json.InstructionJson;
import com.sinevil.tcpTrans.interfaces.DownloadBind;
import com.sinevil.tcpTrans.interfaces.DownloadTaskListener;
import com.sinevil.tcpTrans.tcp.TCPTransNode;
import com.sinevil.tcpTrans.utils.fileTrans.FileTranser;
import com.sinevil.tcpTrans.utils.fileTrans.entity.FilePacketRequestInfo;
import com.sinevil.tcpTrans.utils.log.FileTransLogger;
import lombok.Setter;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Classname FileDownloadManager
 * @Description TODO
 * @Version 1.0.0
 * @Date 2025/1/14 11:07
 * @Created by sinevil
 */
public final class FileDownloadManager {

    private final TCPTransNode transNode;
    /**
     * 下载路径
     * 所有下载文件都应位于此目录下
     */
    private final Path downloadPath;
    /**
     * 下载缓存路径
     */
    private final Path downloadTempPath;

    private final ReentrantLock downloadTaskLock = new ReentrantLock(true);
    private final Condition packetRequestCondition = downloadTaskLock.newCondition();
    /**
     * 启动下载任务
     * 发起数据包请求
     * 和检查传输完成的文件MD5值的线程
     */
    private final Thread packetRequestThread;

    private final ReentrantLock packetLock = new ReentrantLock(true);
    private final Condition prosessCondition = packetLock.newCondition();
    /**
     * 数据包处理线程
     */
    private final Thread packetProsessorThread;
    /**
     * 待处理的文件数据包队列
     */
    private final LinkedBlockingQueue<FilePacketJson> filePacketQueue = new LinkedBlockingQueue<>();
    /**
     * 未启动下载任务字典
     * （filePath : 套接字对象）
     */
    private final ConcurrentHashMap<String, SocketChannel> unStartTaskMap = new ConcurrentHashMap<>();

    /**
     * 正在下载任务字典
     * （下载任务唯一标识：下载任务）
     */
    private final ConcurrentHashMap<String,FileDownloadTask> downloadTaskMap = new ConcurrentHashMap<>();
    private final Map<FileDownloadTask, DownloadBind> downloadBindMap = new HashMap<>();
    /**
     * MD5待校验的队列
     */
    private final LinkedBlockingQueue<FileDownloadTask> unCheckQueue = new LinkedBlockingQueue<>();
    /**
     *  设置下载任务监听器
     *  用于实现下载任务与外部属性绑定
     *
     * @param listener 下载任务监听器
     */
    @Setter
    private DownloadTaskListener listener;

    public FileDownloadManager(TCPTransNode transNode, Path downloadPath,Path downloadTempPath) throws NoSuchAlgorithmException {
        this.transNode = transNode;
        this.downloadPath = downloadPath;
        this.downloadTempPath = downloadTempPath;

        this.packetRequestThread = new Thread(new PacketRequestRunnable(),"FileDownloadPacketRequest");
        this.packetProsessorThread = new Thread(new PacketProcessorRunnable(),"FileDownloadProcessor");

        this.packetRequestThread.start();
        this.packetProsessorThread.start();
    }

    /**
     *  添加一个预下载任务
     *  当正在进行的下载任务不足 MAX_TASK 个时，从未启动任务字典中取出一个任务加入下载任务列表，开始正式下载
     * @param socketChannel 上传端套接字
     * @param filePath 文件路径
     */
    public void downloadFile(SocketChannel socketChannel, String filePath){
        downloadTaskLock.lock();
        try {
            this.unStartTaskMap.put(filePath, socketChannel);
            if (listener != null) {
                this.listener.addTask(filePath);
            }
            this.packetRequestCondition.signal();
        } finally {
            downloadTaskLock.unlock();
        }
    }

    /**
     * 接收到上传端返回的文件基本信息，添加新的下载任务
     * 并移除未启动任务字典中的对应项
     * @param name 文件名
     * @param uid 此次下载任务唯一标识
     * @param fileSize 文件大小
     * @param socketChannel 对应套接字
     * @return 是否添加成功
     */
    public boolean addNewDownloadTask(SocketChannel socketChannel,String name, String uid, long fileSize,String path){
        downloadTaskLock.lock();
        try {
            FileDownloadTask fileDownloadTask = new FileDownloadTask(socketChannel, downloadPath.resolve(name),uid,fileSize);
            downloadTaskMap.put(uid,fileDownloadTask);
            unStartTaskMap.remove(path);
            if (listener!= null)
                this.listener.startTask(fileDownloadTask);
            FileTransLogger.LOGGER.info("开始新的下载任务: "+path);
            packetRequestCondition.signal();
        } catch (IOException e) {
            // 清理残余信息
            downloadTaskMap.remove(uid);
            unStartTaskMap.remove(path);
            return false;
        }finally {
            downloadTaskLock.unlock();
        }
        return true;
    }

    /**
     * 接受文件数据包
     * @param packetJson 文件数据包
     * @return 是否接受成功,如果接受失败则说明对应的下载任务不存在,需通知上传端结束对应任务
     */
    public boolean acceptFilePacket(FilePacketJson packetJson){
        try {
            FileDownloadTask fileDownloadTask = downloadTaskMap.get(packetJson.getUid());
            if(fileDownloadTask != null) {
                if (fileDownloadTask.containsRequest(packetJson.getOffset())) {
                    this.filePacketQueue.put(packetJson);
                    // System.out.println("数据包已加入队列: "+packetJson.uid+ " " + packetJson.offset + " " + packetJson.size);
                    return true;
                }

            }else{
                System.out.println("接收到文件数据包,但不存在对应的下载任务: "+packetJson.uid+ " " + packetJson.offset + " " + packetJson.size);
                return false;
            }
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    /**
     * 接收到上传端返回的文件MD5
     * @param uid 任务唯一表示
     * @param MD5 文件MD5
     */
    public void acceptFileMD5(String uid,String MD5){
        final FileDownloadTask fileDownloadTask = downloadTaskMap.get(uid);
        if(fileDownloadTask != null) {
            fileDownloadTask.uploadMD5 = MD5;
        }
    }

    /**
     * 从已经启动的下载任务列表中取消下载任务
     * @param task 下载任务
     */
    public void cancelDownloadTask(FileDownloadTask task){
        downloadTaskMap.remove(task.uid);
        downloadBindMap.remove(task);
        if (listener!= null)
            this.listener.cancelledTask(null);

        task.closeTask();
    }

    /**
     * 从未启动下载任务列表中取消下载任务
     * @param filePath 文件名
     */
    public void cancelDownloadTask(String filePath){
        this.unStartTaskMap.remove(filePath);
    }

    /**
     * 用于将下载任务与bound绑定
     * 当下载任务的速度和已下载字节数发生变化时，会调用bound的对应方法，实现数值绑定
     * @param task 下载任务
     * @param bound 绑定对象
     */
    public void bindDownloadTask(FileDownloadTask task, DownloadBind bound){
        if (downloadBindMap.containsKey(task)){
            throw new RuntimeException("该任务已绑定过");
        }
        downloadBindMap.put(task,bound);
    }

    /**
     * 处理接收到的数据包的线程
     */
    class PacketProcessorRunnable implements Runnable{
        ByteBuffer byteBuffer = ByteBuffer.allocate(FileTranser.PACKET_SIZE);
        @Override
        public void run() {
            FilePacketJson filePacketJson;
            FileDownloadTask downloadTask;

            while (true) {
                try {
                    filePacketJson = filePacketQueue.take();
                } catch (InterruptedException e) {
                    FileTransLogger.LOGGER.error("数据包处理线程被中断: {}", e.getMessage());
                    // 恢复中断状态
                    Thread.currentThread().interrupt();
                    continue;
                }
                downloadTask = downloadTaskMap.get(filePacketJson.getUid());
                if (downloadTask != null){
                    byteBuffer.clear();
                    byteBuffer.flip();
                    byteBuffer.limit(filePacketJson.size);
                    // 解码数据包为Byte
                    FileTranser.Base64Decoder.decode(filePacketJson.fileData.getBytes(), byteBuffer.array());
                    downloadTask.writePacket(filePacketJson.offset, filePacketJson.size, byteBuffer);
//                    System.out.println("数据包已处理: uid("+filePacketJson.uid+") offset("+filePacketJson.offset+") size("+filePacketJson.size+")");
                    // 检查是否下载完成
                    if (downloadTask.isComplete()){
                        // 下载完成，则向上传端请求MD5进行验证
                        transNode.sendMessage(downloadTask.getSocketChannel(),new InstructionJson(InstructionJson.PUBLIC_REQUEST_FILE_COMPLETE,String.format(InstructionJson.PUBLIC_REQUEST_FILE_COMPLETE_FORMAT,downloadTask.uid,1,"文件下载完成，请求文件MD5验证")));
                        try {
                            // 将任务转移入加入待校验队列
                            downloadTask.checkStarTime = System.currentTimeMillis();
                            unCheckQueue.put(downloadTask);
//                            downloadTaskMap.remove(downloadTask.uid);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }else {
                    // 不存在对应任务
                }

            }
        }
    }

    /**
     * 发起数据包请求的线程
     */
    class PacketRequestRunnable implements Runnable{
        final MessageDigest MD5Util;
        // 请求间隔时间
        final int intervalTime = 5000;
        /**
         * 上一次启动发起请求的的时间
         * 防止过快发起下载请求
         * 当前为5秒
         */
        long lastRequestTime = System.currentTimeMillis()-intervalTime;
        PacketRequestRunnable() throws NoSuchAlgorithmException {
            MD5Util = MessageDigest.getInstance("MD5");
        }

        @Override
        public void run() {
            long currentTime = System.currentTimeMillis();
            int sleepTime;
            int taskNum = 0;

            // 剩余可发起的数据包请求数量
            int residualNum = 0;
            // 平均每个任务课发起的数据包请求数量
            int requestNum = 0;
            while (true){
                downloadTaskLock.lock();
                try {
                    while (unStartTaskMap.isEmpty() && downloadTaskMap.isEmpty()){
                        packetRequestCondition.await();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    downloadTaskLock.unlock();
                }

                // 确保只有至少间隔 intervalTime 的时间才能发起下一轮请求
                currentTime = System.currentTimeMillis();
                sleepTime = (int) (lastRequestTime+intervalTime - currentTime);
                if (sleepTime > 0){
                    try {
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                lastRequestTime = System.currentTimeMillis();


                taskNum = FileTranser.MAX_TASK_NUM - downloadTaskMap.size();
                // 当正在进行的下载任务小于最大任务数时，从预下载任务列表中启动新的下载任务
                if (!unStartTaskMap.isEmpty() && taskNum > 0 ){
                    // 从unStartTaskMap中获取一个任务并启动
                    for (Map.Entry<String, SocketChannel> entry : unStartTaskMap.entrySet()) {
                        String filePath = entry.getKey();
                        SocketChannel socketChannel = entry.getValue();
                        transNode.sendMessage(entry.getValue(),new InstructionJson(InstructionJson.ClIENT_REQUEST_FILE,"file("+filePath+")"));
                        // 减少可启动任务数
                        taskNum--;
                        // 如果已经达到最大任务数，跳出循环
                        if (taskNum <= 0) {
                            break;
                        }
                    }
                    // 更新上一次启动下载任务的时间
                    lastRequestTime = currentTime;

                }

                // 发起数据包请求
                if (!downloadTaskMap.isEmpty()){
                    residualNum = FileTranser.MAX_REQUEST_NUM - filePacketQueue.size();
                    if (residualNum > 0) {
                        requestNum = residualNum/downloadTaskMap.size();
                        for (FileDownloadTask task : downloadTaskMap.values()) {
                            task.speed = task.speedCount.getAndSet(0)/intervalTime*1000;
                            final DownloadBind downloadBind = downloadBindMap.get(task);
                            if (downloadBind != null) {
                                downloadBind.updateSpeed(task.speed);
                                downloadBind.updateWritten(task.writtenByte);
                            }
                            FilePacketRequestInfo[] requestInfos = task.getPacketRequestInfo(requestNum);
                            for (FilePacketRequestInfo requestInfo : requestInfos) {
                                if (requestInfo != null) {
                                    // TODO 打印调试
//                                System.out.println("发起数据包请求：" + requestInfo.toInstructionContent());
                                    transNode.sendMessage(task.getSocketChannel(), new InstructionJson(InstructionJson.PUBLIC_REQUEST_FILE_PACKET, requestInfo.toInstructionContent()));
                                }
                            }
//                            System.out.printf("发送了 %d 个数据包请求 当前已下载 %d MB\n",requestInfos.length, task.writtenByte/FileTranser.MB);
                        }
                    }else {
                        requestNum = 0;
                    }


                }
                // 检查下载完成的文件MD5
                if (!unCheckQueue.isEmpty()){
                    for (FileDownloadTask task : unCheckQueue) {
                        // 如果MD5校验超时，任务失败，当前超时时间为60s
                        if((task.checkStarTime - 60000) > currentTime){
                            transNode.sendMessage(task.getSocketChannel(),new InstructionJson(InstructionJson.PUBLIC_REQUEST_FILE_COMPLETE,String.format(InstructionJson.PUBLIC_REQUEST_FILE_COMPLETE_FORMAT,task.uid,2,"文件MD5校验超时")));
                            task.mark = 3;
                            if (listener!= null)
                                listener.completeTask(task);
                            continue;
                        }
                        // 未超时
                        // 如果已经收到上传端返回的文件MD5
                        if (task.uploadMD5 != null){
                            String md5 =  task.getMD5();
                            System.out.println("md5:"+md5 + "   uploadMD5:"+task.uploadMD5);
                            if (md5.equals(task.uploadMD5)){
                                transNode.sendMessage(task.getSocketChannel(),new InstructionJson(InstructionJson.PUBLIC_REQUEST_FILE_COMPLETE,String.format(InstructionJson.PUBLIC_REQUEST_FILE_COMPLETE_FORMAT,task.uid,2,"验证MD5成功，下载完成")));
                                task.mark = 4;
                                task.completeDownload();
                            }else{
                                transNode.sendMessage(task.getSocketChannel(),new InstructionJson(InstructionJson.PUBLIC_REQUEST_FILE_COMPLETE,String.format(InstructionJson.PUBLIC_REQUEST_FILE_COMPLETE_FORMAT,task.uid,2,"验证MD5失败，下载失败")));
                                task.mark = 3;
                                task.closeTask();
                            }
                            unCheckQueue.remove(task);
                            downloadTaskMap.remove(task.uid);
                            if (listener!= null)
                                listener.completeTask(task);
                        }
                        else{
                            // 未收到上传端返回的文件MD5，重试请求文件MD5
                            transNode.sendMessage(task.getSocketChannel(),new InstructionJson(InstructionJson.PUBLIC_REQUEST_FILE_COMPLETE,String.format(InstructionJson.PUBLIC_REQUEST_FILE_COMPLETE_FORMAT,task.uid,1,"未收到上传端返回MD5，重试请求文件MD5验证")));
                        }
                    }
                }

            }
        }

    }
}